﻿//===============================================================================
// PostSharp Poco implementation for EntityFramework
//===============================================================================
// Copyright © Ruurd Boeke.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PostSharp.Extensibility;
using PostSharp.Laos;
using System.Reflection;
using System.Configuration;
using System.Text.RegularExpressions;
using System.Data.EntityClient;
using System.Data.Metadata.Edm;
using System.IO;
using System.Diagnostics;
using System.Data.Objects.DataClasses;

namespace EntityFrameworkContrib.PostSharp4EF
{
    /// <summary>
    /// <para>
    /// Attribute that can be used to decorate normal POCO classes with. It is used to start off a post compilation phase
    /// that will modify the IL of the class. After this phase, the class will implement:
    /// <list type="">
    /// <item>IEntityWithChangeTracker</item>
    /// <item>IEntityWithKey</item>
    /// <item>IEntityWithRelationships</item>
    /// <item>ISerialization</item>
    /// <item>IStorageAccess</item>
    /// <item>IRelationshipLoader</item>
    /// <item>IUpdateComplexTypesWithTracker</item>
    /// </list>
    /// </para>
    /// <para>
    /// It will also place EDMScalarattributes on your properties.
    /// </para>
    /// <para>This results in a type that is completely ready for consumption by the EntityFramework</para>
    /// </summary>
    [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
    [MulticastAttributeUsage(MulticastTargets.Class, AllowMultiple = false)]
    public sealed class PocoAttribute : CompoundAspect
    {
        #region fields en properties

        /// <summary>
        /// Gets or sets the priority of the property-level aspect.
        /// </summary>
        /// <remarks>
        /// Give a large number to have the event raisen after any other
        /// on-success aspect on the properties of this type. The default value
        /// is 0.
        /// </remarks>
        public int AspectPriority { get { return aspectPriority; } set { aspectPriority = value; } }
        [NonSerialized]
        private int aspectPriority = 0;

        /// <summary>
        /// the name of the container of your edm types
        /// </summary>
        private string EDMContainerName;

        /// <summary>
        /// the name of the type as it exists in the EDM definitions
        /// </summary>
        /// <remarks>if not set, the typename will be used, calculated by taking everything after the last dot (.)</remarks>
        public string Name { get; set; }

        /// <summary>
        /// the namespace of the type as it exists in the EDM definitions
        /// </summary>
        /// <remarks>if not set, the namespace of the type will be used, calculated by taking everything before the last dot (.)</remarks>
        public string NamespaceName { get; set; }


        /// <summary>
        /// the builder
        /// </summary>
        // TODO: this is a side effect, refactor
        EntityConnectionStringBuilder builder;
        #endregion

        #region ctors
        /// <summary>
        /// ctor, needs the name of the container, which should match exactly the connection string name
        /// </summary>
        /// <param name="EDMContainer">the name of the container. This should match exactly your connectionstring name</param>
        public PocoAttribute(string EDMContainerName)
        {
            this.EDMContainerName = EDMContainerName;
        }
        #endregion

        #region the logic
        /// <summary>
        /// provides the aspects
        /// </summary>
        /// <param name="element"></param>
        /// <param name="collection"></param>
        public override void ProvideAspects(object element, LaosReflectionAspectCollection collection)
        {
            // Get the target type.
            Type typeDef = (Type)element;

            // get the edm workspace
            MetadataWorkspace metadata = GetMetaDataWorkspace(typeDef.Assembly.Location);

            // we need our entity or complextype so we can loop through it's members
            EntityType entityType;
            ComplexType complexType;
            bool IsEntityType = metadata.TryGetItem<EntityType>(BuildCompleteTypeName(typeDef.FullName), DataSpace.CSpace, out entityType);
            if(IsEntityType)
            {
                ProvideEntityTypeAspects(entityType, typeDef, metadata, collection);
            }
            else
            {
                // will throw if not found.. correct behavior
                complexType = metadata.GetItem<ComplexType>(BuildCompleteTypeName(typeDef.FullName), DataSpace.CSpace);

                ProvideComplexTypeAspects(complexType, typeDef, metadata, collection);
            }
            

            // inspect the complete class and enhance it for use with EDM  ( attributes and defaultvalues setting)
            collection.AddAspect(typeDef, new EDMSubAspect(builder.Metadata, builder.Provider));

            // implement the IRelationShipLoader interface on the class
            collection.AddAspect(typeDef, new RelationshipLoaderSubAspect());

            // implement helper class to get and set original values
            collection.AddAspect(typeDef, new SerializationHelpSubAspect());

            #region add fieldaccess rewriting for navigational properties
            if (IsEntityType)    // complex types do not support navigation properties
            {
                foreach (NavigationProperty navigationProperty in entityType.NavigationProperties)
                {
                    FieldInfo field = GetFieldBelongingToProperty(typeDef, navigationProperty.Name);

                    if (field != null)
                    {
                        // TODO: notify user with a message that this field could not be processed

                        if (navigationProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
                        {
                            // I can't find a way to get to the field type, even though we have a field.. that is generic and
                            // somehow I can not get the generic type out of it
                            // TODO: look at this code again.
                            TypeUsage typeUsage = navigationProperty.ToEndMember.MetadataProperties.First(prop => prop.Name == "TypeUsage").Value as TypeUsage;
                            string toMemberTypeName = typeUsage.EdmType.Name.Replace("reference[", "").Replace("]", "");
                            Type target = typeDef.Assembly.GetType(toMemberTypeName);

                            collection.AddAspect(field,
                              new OnRelatedCollectionFieldAspect(navigationProperty.RelationshipType.FullName, navigationProperty.ToEndMember.Name, target));
                        }
                        else
                        {
                            // TODO: find out if there is diffence in behaviour between 0..1 or 1
                            collection.AddAspect(field,
                              new OnRelatedReferenceFieldAspect(navigationProperty.RelationshipType.FullName, navigationProperty.ToEndMember.Name, field.FieldType));
                        }
                    }
                }
            }
            #endregion
        }

        void ProvideEntityTypeAspects(EntityType entityType, Type typeDef, MetadataWorkspace metadata, LaosReflectionAspectCollection collection)
        {

            // implement the three IPOCO interfaces on the class
            collection.AddAspect(typeDef, new PocoEntityInterfacesSubAspect());

            #region add changetracking to valuetype properties
            foreach (EdmProperty simpleProperty in entityType.Properties)
            {
                MethodInfo method = typeDef.GetProperty(simpleProperty.Name).GetSetMethod();

                // this property could be pointing to a real property or to a complex type. we need to differentiate
                if (simpleProperty.TypeUsage.EdmType is ComplexType)
                {
                    // it is a property pointing to a complex type
                    collection.AddAspect(method, new OnComplexPropertySetChangeTrackSubAspect(simpleProperty.Name, this.AspectPriority));
                }
                else
                {
                    // only do properties that actually belong to this type
                    if (((EdmMember) simpleProperty).DeclaringType == entityType)
                    {
                        // it is a regular property
                        collection.AddAspect(method, new OnEntityPropertySetChangeTrackSubAspect(simpleProperty.Name, this.AspectPriority));
                    }
                }

                // TODO: are there other edmtypes that need to be taken care of?
            }
            #endregion

        }

        void ProvideComplexTypeAspects(ComplexType complexType, Type typeDef, MetadataWorkspace metadata, LaosReflectionAspectCollection collection)
        {
            // implement the IComplexType interfaces on the class, giving it methods to set it's changetracker.. 
            collection.AddAspect(typeDef, new PocoComplexInterfacesSubAspect());
        }

        #endregion

        #region helpers
        private string FindConfigPath(string assembly)
        {

            // search to the root
            string assemblypath = assembly.Substring(0, assembly.LastIndexOf(Path.DirectorySeparatorChar));
            while (assemblypath.Contains(Path.DirectorySeparatorChar))
            {
                assemblypath = assemblypath.Substring(0, assemblypath.LastIndexOf(Path.DirectorySeparatorChar));

                // TODO: get a better config story.

                string pathUnderTest = assemblypath + Path.DirectorySeparatorChar + Path.DirectorySeparatorChar + "App.Config";

                if (File.Exists(pathUnderTest))
                    return pathUnderTest;
            }

            return null;
        }

        private MetadataWorkspace GetMetaDataWorkspace(string assemblyLocation)
        {
            ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
            string configLocation = FindConfigPath(assemblyLocation);
            fileMap.ExeConfigFilename = configLocation;
            // TODO: throw compilation error when not valid file
            Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);

            ConnectionStringSettingsCollection connections = configuration.ConnectionStrings.ConnectionStrings;
            string connectionString = connections[EDMContainerName].ConnectionString;


            // TODO: do this better and do not count on group order
            Regex r = new Regex("metadata=(.*?);");
            Match metadata = r.Match(connectionString);
            Debug.Assert(metadata.Success);
            string metadatastring = metadata.Groups[1].Value + "|";

            r = new Regex(@".*?\|");
            MatchCollection matches = r.Matches(metadatastring);
            string csdl = matches[0].Value.Replace("|", "");
            string ssdl = matches[1].Value.Replace("|", "");
            string msl = matches[2].Value.Replace("|", "");

            r = new Regex(@"provider=(.*?);");
            string provider = r.Match(connectionString).Groups[1].Value;

            string rootdir = configLocation.Substring(0, configLocation.LastIndexOf(Path.DirectorySeparatorChar));

            builder = new EntityConnectionStringBuilder();
            builder.Metadata = string.Format("{0}|{1}|{2}",
              rootdir + Path.DirectorySeparatorChar + csdl,
              rootdir + Path.DirectorySeparatorChar + ssdl,
              rootdir + Path.DirectorySeparatorChar + msl);
            builder.Provider = provider;

            EntityConnection connection = new EntityConnection(builder.ConnectionString);
            // get to the metadata of the type we wish to transform
            return connection.GetMetadataWorkspace();
        }

        private string BuildCompleteTypeName(string elementTypeName)
        {
            return this.NamespaceNameCalculation(elementTypeName) + "." + this.TypeNameCalculation(elementTypeName);
        }

        /// <summary>
        /// either uses the set namespace, or calculate it
        /// </summary>
        public string NamespaceNameCalculation(string elementTypeName)
        {

            if (!String.IsNullOrEmpty(this.NamespaceName))
                return this.NamespaceName;
            else
                return elementTypeName.Substring(0, elementTypeName.LastIndexOf("."));
        }

        /// <summary>
        /// either uses the set name, or calculate it
        /// </summary>
        public string TypeNameCalculation(string elementTypeName)
        {
            if (!String.IsNullOrEmpty(this.Name))
                return this.Name;
            else
                return elementTypeName.Substring(elementTypeName.LastIndexOf(".") + 1);
        }

        public FieldInfo GetFieldBelongingToProperty(Type targetType, string propertyName)
        {
            FieldInfo fld;
            FieldInfo[] allFields = targetType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

            // the rules: the field must match the ending of the propertyname. So underscore is okay
            fld = allFields.SingleOrDefault(field => field.Name.EndsWith(propertyName, StringComparison.OrdinalIgnoreCase));

            // in case that didn't match, try the autogenerated fieldname
            if (fld == null)
            {
                fld = allFields.SingleOrDefault(field => (field.Name.IndexOf("<" + propertyName + ">") == 0));
            }

            return fld;
        }

        #endregion
    }
}
